/* global NodeJS */

import type { MacAddress } from 'hap-nodejs'

import type { AccessoryPlugin, PlatformPlugin } from './api.js'
import type {
  AccessoryConfig,
  BridgeConfiguration,
  BridgeOptions,
  HomebridgeConfig,
  PlatformConfig,
} from './bridgeService.js'
import type {
  ChildBridgePairedStatusEventData,
  ChildProcessLoadEventData,
  ChildProcessMessageEvent,
  ChildProcessPluginLoadedEventData,
  ChildProcessPortAllocatedEventData,
  ChildProcessPortRequestEventData,
} from './childBridgeService.js'
import type { Plugin } from './plugin.js'

import process from 'node:process'

import { AccessoryEventTypes, HAPStorage } from 'hap-nodejs'

import { HomebridgeAPI, PluginType } from './api.js'
import { BridgeService } from './bridgeService.js'
import { ChildProcessMessageEventType } from './childBridgeService.js'
import { ChildBridgeExternalPortService } from './externalPortService.js'
import { Logger } from './logger.js'
import { PluginManager } from './pluginManager.js'
import { User } from './user.js'

import 'source-map-support/register.js'

/**
 * This is a standalone script executed as a child process fork
 */

process.title = 'homebridge: child bridge'

export class ChildBridgeFork {
  private bridgeService!: BridgeService
  private api!: HomebridgeAPI
  private pluginManager!: PluginManager
  private externalPortService!: ChildBridgeExternalPortService
  private matterServer?: import('./matter/matterServer.js').MatterServer

  private matterConfig?: { port?: number }
  private matterSerialNumber?: string

  private type!: PluginType
  private plugin!: Plugin
  private identifier!: string
  private pluginConfig!: Array<PlatformConfig | AccessoryConfig>
  private bridgeConfig!: BridgeConfiguration
  private bridgeOptions!: BridgeOptions
  private homebridgeConfig!: HomebridgeConfig

  private portRequestCallback: Map<MacAddress, (port: number | undefined) => void> = new Map()

  constructor() {
    // tell the parent process we are ready to accept plugin config
    this.sendMessage(ChildProcessMessageEventType.READY)
  }

  sendMessage<T = unknown>(type: ChildProcessMessageEventType, data?: T): void {
    if (process.send) {
      process.send({
        id: type,
        data,
      })
    }
  }

  async loadPlugin(data: ChildProcessLoadEventData): Promise<void> {
    // set data
    this.type = data.type
    this.identifier = data.identifier
    this.pluginConfig = data.pluginConfig
    this.bridgeConfig = data.bridgeConfig
    this.bridgeOptions = data.bridgeOptions
    this.homebridgeConfig = data.homebridgeConfig

    // remove the _bridge key (some plugins do not like unknown config)
    for (const config of this.pluginConfig) {
      delete config._bridge
    }

    // set bridge settings (inherited from main bridge)
    if (this.bridgeOptions.noLogTimestamps) {
      Logger.setTimestampEnabled(false)
    }

    if (this.bridgeOptions.debugModeEnabled) {
      Logger.setDebugEnabled(true)
    }

    if (this.bridgeOptions.forceColourLogging) {
      Logger.forceColor()
    }

    // Initialize HAP-NodeJS with a custom persist directory
    HAPStorage.setCustomStoragePath(User.persistPath())

    // load api
    this.api = new HomebridgeAPI()
    this.pluginManager = new PluginManager(this.api)
    this.externalPortService = new ChildBridgeExternalPortService(this)

    // load plugin
    this.plugin = this.pluginManager.loadPlugin(data.pluginPath)
    await this.plugin.load()
    await this.pluginManager.initializePlugin(this.plugin, data.identifier)

    // change process title to include plugin name
    process.title = `homebridge: ${this.plugin.getPluginIdentifier()}`

    this.sendMessage<ChildProcessPluginLoadedEventData>(ChildProcessMessageEventType.LOADED, {
      version: this.plugin.version,
    })
  }

  async startBridge(): Promise<void> {
    // Check if Matter is configured
    this.matterConfig = this.bridgeConfig.matter

    // Initialize Matter if it's configured
    if (this.matterConfig) {
      Logger.internal.debug('Child bridge has Matter config (Combined HAP+Matter), starting Matter server')

      // If Matter doesn't have a port configured, allocate one
      if (!this.matterConfig.port) {
        // Generate a unique username for Matter port allocation
        const matterUsername = `${this.bridgeConfig.username}:MATTER` as MacAddress
        const matterPort = await this.externalPortService.requestPort(matterUsername)

        if (!matterPort) {
          throw new Error(
            'Failed to allocate Matter port for child bridge. '
            + 'Please specify a port manually in the _bridge.matter configuration, or free up ports in the configured range.',
          )
        }

        this.matterConfig.port = matterPort
        Logger.internal.debug(`Allocated Matter port: ${this.matterConfig.port} (HAP port: ${this.bridgeConfig.port})`)
      }

      // Start Matter server
      await this.startMatterServer(this.matterConfig)

      // Send Matter bridge status to parent process
      this.sendMatterStatusEvent()
    }

    this.bridgeService = new BridgeService(
      this.api,
      this.pluginManager,
      this.externalPortService,
      this.bridgeOptions,
      this.bridgeConfig,
    )

    // watch bridge events to check when server is online
    this.bridgeService.bridge.on(AccessoryEventTypes.ADVERTISED, () => {
      this.sendPairedStatusEvent()
    })

    // watch for the paired event to update the server status
    this.bridgeService.bridge.on(AccessoryEventTypes.PAIRED, () => {
      this.sendPairedStatusEvent()
    })

    // watch for the unpaired event to update the server status
    this.bridgeService.bridge.on(AccessoryEventTypes.UNPAIRED, () => {
      this.sendPairedStatusEvent()
    })

    // load the cached accessories
    await this.bridgeService.loadCachedPlatformAccessoriesFromDisk()

    // For combined HAP+Matter bridges, we should only process the platform once
    // When _bridge.matter is configured alongside HAP, we only need to instantiate the platform once
    let platformInstantiated = false

    for (const config of this.pluginConfig) {
      if (this.type === PluginType.PLATFORM) {
        // Skip if we've already instantiated this platform (for combined HAP+Matter)
        if (platformInstantiated) {
          continue
        }

        const plugin = this.pluginManager.getPluginForPlatform(this.identifier)
        const displayName = config.name || plugin.getPluginIdentifier()
        const logger = Logger.withPrefix(displayName)
        const constructor = plugin.getPlatformConstructor(this.identifier)
        const platform: PlatformPlugin = new constructor(logger, config as PlatformConfig, this.api)

        if (HomebridgeAPI.isDynamicPlatformPlugin(platform)) {
          plugin.assignDynamicPlatform(this.identifier, platform)
          platformInstantiated = true
        } else if (HomebridgeAPI.isStaticPlatformPlugin(platform)) { // Plugin 1.0, load accessories
          await this.bridgeService.loadPlatformAccessories(plugin, platform, this.identifier, logger)
          platformInstantiated = true
        } else {
          // otherwise it's a IndependentPlatformPlugin which doesn't expose any methods at all.
          // We just call the constructor and let it be enabled.
          platformInstantiated = true
        }
      } else if (this.type === PluginType.ACCESSORY) {
        const plugin = this.pluginManager.getPluginForAccessory(this.identifier)
        const displayName = config.name

        if (!displayName) {
          Logger.internal.warn('Could not load accessory %s as it is missing the required \'name\' property!', this.identifier)
          return
        }

        const logger = Logger.withPrefix(displayName)
        const constructor = plugin.getAccessoryConstructor(this.identifier)
        const accessoryInstance: AccessoryPlugin = new constructor(logger, config as AccessoryConfig, this.api)

        // pass accessoryIdentifier for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation
        const accessory = this.bridgeService.createHAPAccessory(plugin, accessoryInstance, displayName, this.identifier, config.uuid_base)

        if (accessory) {
          this.bridgeService.bridge.addBridgedAccessory(accessory)
        } else {
          logger('Accessory %s returned empty set of services. Won\'t adding it to the bridge!', this.identifier)
        }
      }
    }

    // restore the cached accessories
    this.bridgeService.restoreCachedPlatformAccessories()

    this.bridgeService.publishBridge()
    this.api.signalFinished()

    // tell the parent we are online
    this.sendMessage(ChildProcessMessageEventType.ONLINE)
  }

  /**
   * Request the next available external port from the parent process
   * @param username
   */
  public async requestExternalPort(username: MacAddress): Promise<number | undefined> {
    return new Promise((resolve) => {
      const requestTimeout = setTimeout(() => {
        Logger.internal.warn('Parent process did not respond to port allocation request within 5 seconds - assigning random port.')
        resolve(undefined)
      }, 5000)

      // setup callback
      const callback = (port: number | undefined) => {
        clearTimeout(requestTimeout)
        resolve(port)
        this.portRequestCallback.delete(username)
      }
      this.portRequestCallback.set(username, callback)

      // send port request
      this.sendMessage<ChildProcessPortRequestEventData>(ChildProcessMessageEventType.PORT_REQUEST, { username })
    })
  }

  /**
   * Handles the port allocation response message from the parent process
   * @param data
   */
  public handleExternalResponse(data: ChildProcessPortAllocatedEventData): void {
    const callback = this.portRequestCallback.get(data.username)
    if (callback) {
      callback(data.port)
    }
  }

  /**
   * Sends the current pairing status of the child bridge to the parent process
   */
  public sendPairedStatusEvent() {
    this.sendMessage<ChildBridgePairedStatusEventData>(ChildProcessMessageEventType.STATUS_UPDATE, {
      paired: this.bridgeService?.bridge?._accessoryInfo?.paired() ?? null,
      setupUri: this.bridgeService?.bridge?.setupURI() ?? null,
    })
  }

  /**
   * Sends Matter bridge status when Matter is configured
   */
  public sendMatterStatusEvent() {
    if (!this.matterConfig) {
      return
    }

    // Generate identifier from username
    const matterBridgeId = `${this.bridgeConfig.username}-matter`

    const commissioningInfo = this.matterServer?.getCommissioningInfo() ?? { commissioned: false }
    const { qrCode, manualPairingCode, commissioned } = commissioningInfo

    this.sendMessage(ChildProcessMessageEventType.MATTER_STATUS_UPDATE, {
      type: 'matter' as const,
      status: commissioned ? 'ok' : 'pending',
      port: this.matterConfig.port || 5540,
      qrCode,
      manualPairingCode,
      serialNumber: this.matterSerialNumber,
      plugin: this.plugin.getPluginIdentifier(),
      identifier: matterBridgeId,
      username: this.bridgeConfig.username,
      deviceCount: this.matterServer?.getAccessories().length ?? 0,
      manuallyStopped: false,
      pid: process.pid,
      commissioned,
    })

    Logger.internal.debug(`Sent Matter status update for bridge: ${matterBridgeId}`)
  }

  /**
   * Start Matter server for child bridge
   */
  private async startMatterServer(matterConfig: any): Promise<void> {
    Logger.internal.info('Starting Matter server in child bridge process')

    // Dynamically import the MatterServer to avoid loading it when not needed
    const { MatterServer } = await import('./matter/matterServer.js')

    // Generate unique ID from username
    const uniqueId = `${this.bridgeConfig.username}-matter`

    // Create Matter server with the provided configuration
    this.matterServer = new MatterServer({
      port: matterConfig.port || 5540,
      uniqueId,
      storagePath: User.matterPath(),
      // Inherit debug mode from bridge options
      debugModeEnabled: this.bridgeOptions.debugModeEnabled,
      // Inherit configuration from bridge config
      manufacturer: this.bridgeConfig.manufacturer,
      model: this.bridgeConfig.model,
      firmwareRevision: this.bridgeConfig.firmwareRevision,
      serialNumber: this.bridgeConfig.serialNumber,
    })

    await this.matterServer.start()

    // Inform the API that Matter is enabled
    this.api._setMatterEnabled(true)

    // Set the Matter server reference for API methods like getAccessoryState
    this.api._setMatterServer(this.matterServer)

    const commissioningInfo = this.matterServer.getCommissioningInfo()
    Logger.internal.info('Matter server started with commissioning info:', commissioningInfo)

    // Store the serial number for status updates
    this.matterSerialNumber = commissioningInfo.serialNumber

    // Send initial Matter status update
    this.sendMatterStatusEvent()

    // Set up event listeners for Matter API calls
    const { InternalAPIEvent } = await import('./api.js')
    this.api.on(InternalAPIEvent.REGISTER_MATTER_ACCESSORY, (accessory: any) => {
      if (this.matterServer) {
        this.matterServer.registerAccessory(accessory)
      }
    })
    this.api.on(InternalAPIEvent.UNREGISTER_MATTER_ACCESSORY, (uuid: string) => {
      if (this.matterServer) {
        this.matterServer.unregisterAccessory(uuid)
      }
    })
    this.api.on(InternalAPIEvent.UPDATE_MATTER_ACCESSORY_STATE, (uuid: string, cluster: string, attributes: Record<string, any>) => {
      if (this.matterServer) {
        this.matterServer.updateAccessoryState(uuid, cluster, attributes)
      }
    })

    // Send updated Matter status with fresh commissioning info to parent
    this.sendMatterStatusEvent()
  }

  /**
   * Handle Matter metadata request from parent process
   */
  public handleMatterMetadataRequest(): void {
    if (!this.matterConfig || !this.matterServer) {
      return
    }

    // Send Matter bridge metadata back to parent
    this.sendMatterStatusEvent()
  }

  shutdown(): void {
    this.bridgeService.teardown()
    // Only stop Matter server if it was initialized
    if (this.matterServer && typeof this.matterServer.stop === 'function') {
      Logger.internal.debug('Stopping Matter server')
      this.matterServer.stop().catch((error: any) => {
        Logger.internal.error('Error stopping Matter server:', error)
      })
    }
  }
}

/**
 * Start Self
 */
const childPluginFork = new ChildBridgeFork()

/**
 * Handle incoming IPC messages from the parent Homebridge process
 */
process.on('message', (message: ChildProcessMessageEvent<unknown>) => {
  if (typeof message !== 'object' || !message.id) {
    return
  }

  switch (message.id) {
    case ChildProcessMessageEventType.LOAD: {
      childPluginFork.loadPlugin(message.data as ChildProcessLoadEventData)
      break
    }
    case ChildProcessMessageEventType.START: {
      childPluginFork.startBridge()
      break
    }
    case ChildProcessMessageEventType.PORT_ALLOCATED: {
      childPluginFork.handleExternalResponse(message.data as ChildProcessPortAllocatedEventData)
      break
    }
    case 'MATTER_BRIDGE_METADATA_REQUEST' as any: {
      // Handle request for Matter bridge metadata
      childPluginFork.handleMatterMetadataRequest()
      break
    }
  }
})

/**
 * Handle the sigterm shutdown signals
 */
let shuttingDown = false
function signalHandler(signal: NodeJS.Signals, signalNum: number): void {
  if (shuttingDown) {
    return
  }
  shuttingDown = true

  Logger.internal.info('Got %s, shutting down child bridge process...', signal)

  try {
    childPluginFork.shutdown()
  } catch (error: any) {
    // do nothing
  }

  setTimeout(() => process.exit(128 + signalNum), 5000)
}

process.on('SIGINT', signalHandler.bind(undefined, 'SIGINT', 2))
process.on('SIGTERM', signalHandler.bind(undefined, 'SIGTERM', 15))

/**
 * Ensure orphaned processes are cleaned up
 */
setInterval(() => {
  if (!process.connected) {
    Logger.internal.info('Parent process not connected, terminating process...')
    process.exit(1)
  }
}, 5000)
